#!/usr/bin/env python3

# Copyright 2015 gRPC authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import argparse
import datetime
import os
import re
import subprocess
import sys

# find our home
ROOT = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), "../.."))
os.chdir(ROOT)

# parse command line
argp = argparse.ArgumentParser(description="copyright checker")
argp.add_argument(
    "-o", "--output", default="details", choices=["list", "details"]
)
argp.add_argument("-s", "--skips", default=0, action="store_const", const=1)
argp.add_argument("-a", "--ancient", default=0, action="store_const", const=1)
argp.add_argument("--precommit", action="store_true")
argp.add_argument("--fix", action="store_true")
args = argp.parse_args()

# open the license text
with open("NOTICE.txt") as f:
    LICENSE_NOTICE = f.read().splitlines()

# license format by file extension
# key is the file extension, value is a format string
# that given a line of license text, returns what should
# be in the file
LICENSE_PREFIX_RE = {
    ".bat": r"@rem\s*",
    ".c": r"\s*(?://|\*)\s*",
    ".cc": r"\s*(?://|\*)\s*",
    ".h": r"\s*(?://|\*)\s*",
    ".m": r"\s*\*\s*",
    ".mm": r"\s*\*\s*",
    ".php": r"\s*\*\s*",
    ".js": r"\s*\*\s*",
    ".py": r"#\s*",
    ".pyx": r"#\s*",
    ".pxd": r"#\s*",
    ".pxi": r"#\s*",
    ".rb": r"#\s*",
    ".sh": r"#\s*",
    ".proto": r"//\s*",
    ".cs": r"//\s*",
    ".mak": r"#\s*",
    ".bazel": r"#\s*",
    ".bzl": r"#\s*",
    "Makefile": r"#\s*",
    "Dockerfile": r"#\s*",
    "BUILD": r"#\s*",
}

# The key is the file extension, while the value is a tuple of fields
# (header, prefix, footer).
# For example, for javascript multi-line comments, the header will be '/*', the
# prefix will be '*' and the footer will be '*/'.
# If header and footer are irrelevant for a specific file extension, they are
# set to None.
LICENSE_PREFIX_TEXT = {
    ".bat": (None, "@rem", None),
    ".c": (None, "//", None),
    ".cc": (None, "//", None),
    ".h": (None, "//", None),
    ".m": ("/**", " *", " */"),
    ".mm": ("/**", " *", " */"),
    ".php": ("/**", " *", " */"),
    ".js": ("/**", " *", " */"),
    ".py": (None, "#", None),
    ".pyx": (None, "#", None),
    ".pxd": (None, "#", None),
    ".pxi": (None, "#", None),
    ".rb": (None, "#", None),
    ".sh": (None, "#", None),
    ".proto": (None, "//", None),
    ".cs": (None, "//", None),
    ".mak": (None, "#", None),
    ".bazel": (None, "#", None),
    ".bzl": (None, "#", None),
    "Makefile": (None, "#", None),
    "Dockerfile": (None, "#", None),
    "BUILD": (None, "#", None),
}

_EXEMPT = frozenset(
    (
        # Generated protocol compiler output.
        "examples/python/helloworld/helloworld_pb2.py",
        "examples/python/helloworld/helloworld_pb2_grpc.py",
        "examples/python/multiplex/helloworld_pb2.py",
        "examples/python/multiplex/helloworld_pb2_grpc.py",
        "examples/python/multiplex/route_guide_pb2.py",
        "examples/python/multiplex/route_guide_pb2_grpc.py",
        "examples/python/route_guide/route_guide_pb2.py",
        "examples/python/route_guide/route_guide_pb2_grpc.py",
        # Generated doxygen config file
        "tools/doxygen/Doxyfile.php",
        # An older file originally from outside gRPC.
        "src/php/tests/bootstrap.php",
        # census.proto copied from github
        "tools/grpcz/census.proto",
        # status.proto copied from googleapis
        "src/proto/grpc/status/status.proto",
        # Gradle wrappers used to build for Android
        "examples/android/helloworld/gradlew.bat",
        "src/android/test/interop/gradlew.bat",
        # Designer-generated source
        "examples/csharp/HelloworldXamarin/Droid/Resources/Resource.designer.cs",
        "examples/csharp/HelloworldXamarin/iOS/ViewController.designer.cs",
        # BoringSSL generated header. It has commit version information at the head
        # of the file so we cannot check the license info.
        "src/boringssl/boringssl_prefix_symbols.h",
    )
)

_ENFORCE_CPP_STYLE_COMMENT_PATH_PREFIX = tuple(
    [
        "include/grpc++/",
        "include/grpcpp/",
        "src/core/",
        "src/cpp/",
        "test/core/",
        "test/cpp/",
        "fuzztest/",
    ]
)

RE_YEAR = (
    r"Copyright (?P<first_year>[0-9]+\-)?(?P<last_year>[0-9]+) ([Tt]he )?gRPC"
    r" [Aa]uthors(\.|)"
)
RE_LICENSE = dict(
    (
        k,
        r"\n".join(
            LICENSE_PREFIX_RE[k]
            + (RE_YEAR if re.search(RE_YEAR, line) else re.escape(line))
            for line in LICENSE_NOTICE
        ),
    )
    for k, v in list(LICENSE_PREFIX_RE.items())
)

RE_C_STYLE_COMMENT_START = r"^/\*\s*\n"
RE_C_STYLE_COMMENT_OPTIONAL_LINE = r"(?:\s*\*\s*\n)*"
RE_C_STYLE_COMMENT_END = r"\s*\*/"
RE_C_STYLE_COMMENT_LICENSE = (
    RE_C_STYLE_COMMENT_START
    + RE_C_STYLE_COMMENT_OPTIONAL_LINE
    + r"\n".join(
        r"\s*(?:\*)\s*"
        + (RE_YEAR if re.search(RE_YEAR, line) else re.escape(line))
        for line in LICENSE_NOTICE
    )
    + r"\n"
    + RE_C_STYLE_COMMENT_OPTIONAL_LINE
    + RE_C_STYLE_COMMENT_END
)
RE_CPP_STYLE_COMMENT_LICENSE = r"\n".join(
    r"\s*(?://)\s*" + (RE_YEAR if re.search(RE_YEAR, line) else re.escape(line))
    for line in LICENSE_NOTICE
)

YEAR = datetime.datetime.now().year

LICENSE_YEAR = f"Copyright {YEAR} gRPC authors."


def join_license_text(header, prefix, footer, notice):
    text = (header + "\n") if header else ""

    def add_prefix(prefix, line):
        # Don't put whitespace between prefix and empty line to avoid having
        # trailing whitespaces.
        return prefix + ("" if len(line) == 0 else " ") + line

    text += "\n".join(
        add_prefix(prefix, (LICENSE_YEAR if re.search(RE_YEAR, line) else line))
        for line in LICENSE_NOTICE
    )
    text += "\n"
    if footer:
        text += footer + "\n"
    return text


LICENSE_TEXT = dict(
    (
        k,
        join_license_text(
            LICENSE_PREFIX_TEXT[k][0],
            LICENSE_PREFIX_TEXT[k][1],
            LICENSE_PREFIX_TEXT[k][2],
            LICENSE_NOTICE,
        ),
    )
    for k, v in list(LICENSE_PREFIX_TEXT.items())
)

if args.precommit:
    FILE_LIST_COMMAND = (
        "git status -z | grep -Poz '(?<=^[MARC][MARCD ] )[^\s]+'"
    )
else:
    FILE_LIST_COMMAND = (
        "git ls-tree -r --name-only -r HEAD | "
        "grep -v ^third_party/ |"
        'grep -v "\(ares_config.h\|ares_build.h\)"'
    )


def load(name):
    with open(name) as f:
        return f.read()


def save(name, text):
    with open(name, "w") as f:
        f.write(text)


assert re.search(RE_LICENSE["Makefile"], load("Makefile"))


def log(cond, why, filename):
    if not cond:
        return
    if args.output == "details":
        print(("%s: %s" % (why, filename)))
    else:
        print(filename)


def write_copyright(license_text, file_text, filename):
    shebang = ""
    lines = file_text.split("\n")
    if lines and lines[0].startswith("#!"):
        shebang = lines[0] + "\n"
        file_text = file_text[len(shebang) :]

    rewritten_text = shebang + license_text + "\n" + file_text
    with open(filename, "w") as f:
        f.write(rewritten_text)


def replace_copyright(license_text, file_text, filename):
    m = re.search(RE_C_STYLE_COMMENT_LICENSE, text)
    if m:
        rewritten_text = license_text + file_text[m.end() :]
        with open(filename, "w") as f:
            f.write(rewritten_text)
        return True
    return False


# scan files, validate the text
ok = True
filename_list = []
try:
    filename_list = (
        subprocess.check_output(FILE_LIST_COMMAND, shell=True)
        .decode()
        .splitlines()
    )
except subprocess.CalledProcessError:
    sys.exit(0)

for filename in filename_list:
    enforce_cpp_style_comment = False
    if filename in _EXEMPT:
        continue
    # Skip check for upb generated code.
    if (
        filename.endswith(".upb.h")
        or filename.endswith(".upbdefs.h")
        or filename.endswith(".upbdefs.c")
        or filename.endswith(".upb_minitable.h")
        or filename.endswith(".upb_minitable.c")
    ):
        continue
    # Allow empty __init__.py files for code generated by xds_protos
    if filename.startswith("tools/distrib/python/xds_protos") and (
        filename.endswith("__init__.py")
        or filename.endswith("generated_file_import_test.py")
    ):
        continue
    ext = os.path.splitext(filename)[1]
    base = os.path.basename(filename)
    if filename.startswith(_ENFORCE_CPP_STYLE_COMMENT_PATH_PREFIX) and ext in [
        ".cc",
        ".h",
    ]:
        enforce_cpp_style_comment = True
        re_license = RE_CPP_STYLE_COMMENT_LICENSE
        license_text = LICENSE_TEXT[ext]
    elif ext in RE_LICENSE:
        re_license = RE_LICENSE[ext]
        license_text = LICENSE_TEXT[ext]
    elif base in RE_LICENSE:
        re_license = RE_LICENSE[base]
        license_text = LICENSE_TEXT[base]
    else:
        log(args.skips, "skip", filename)
        continue
    try:
        text = load(filename)
    except:
        continue
    m = re.search(re_license, text)
    if m:
        pass
    elif enforce_cpp_style_comment:
        log(
            1,
            "copyright missing or does not use cpp-style copyright header",
            filename,
        )
        if args.fix:
            # Attempt fix: search for c-style copyright header and replace it
            # with cpp-style copyright header. If that doesn't work
            # (e.g. missing copyright header), write cpp-style copyright header.
            if not replace_copyright(license_text, text, filename):
                write_copyright(license_text, text, filename)
        ok = False
    elif "DO NOT EDIT" not in text:
        if args.fix:
            write_copyright(license_text, text, filename)
            log(1, "copyright missing (fixed)", filename)
        else:
            log(1, "copyright missing", filename)
        ok = False

if not ok and not args.fix:
    print(
        "You may use following command to automatically fix copyright headers:"
    )
    print("    tools/distrib/check_copyright.py --fix")

sys.exit(0 if ok else 1)
